{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Generalizing our definition of vectors"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Creating a class for 2D coordinate vectors"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "source": [
    "class Vec2():\n",
    "    def __init__(self,x,y):\n",
    "        self.x = x\n",
    "        self.y = y\n",
    "    def add(self, v2):\n",
    "        return Vec2(self.x + v2.x, self.y + v2.y)"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "source": [
    "v = Vec2(3,4) # <1>\n",
    "w = v.add(Vec2(-2,6)) # <2>\n",
    "print(w.x) # <3>"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "source": [
    "class Vec2():\n",
    "    def __init__(self,x,y):\n",
    "        self.x = x\n",
    "        self.y = y\n",
    "    def add(self, v2):\n",
    "        return Vec2(self.x + v2.x, self.y + v2.y)\n",
    "    def scale(self, scalar):\n",
    "        return Vec2(scalar * self.x, scalar * self.y)\n",
    "    def __eq__(self,other):\n",
    "        return self.x == other.x and self.y == other.y"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Improving the Vec2 class"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "source": [
    "class Vec2():\n",
    "    def __init__(self,x,y):\n",
    "        self.x = x\n",
    "        self.y = y\n",
    "    def add(self, v2):\n",
    "        return Vec2(self.x + v2.x, self.y + v2.y)\n",
    "    def scale(self, scalar):\n",
    "        return Vec2(scalar * self.x, scalar * self.y)\n",
    "    def __eq__(self,other):\n",
    "        return self.x == other.x and self.y == other.y\n",
    "    def __add__(self, v2):\n",
    "        return self.add(v2)\n",
    "    def __mul__(self, scalar):\n",
    "        return self.scale(scalar)\n",
    "    def __rmul__(self,scalar): #1\n",
    "        return self.scale(scalar)"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "source": [
    "# nice syntax\n",
    "3.0 * Vec2(1,0) + 4.0 * Vec2(0,1)"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "source": [
    "class Vec2():\n",
    "    def __init__(self,x,y):\n",
    "        self.x = x\n",
    "        self.y = y\n",
    "    def add(self, v2):\n",
    "        return Vec2(self.x + v2.x, self.y + v2.y)\n",
    "    def scale(self, scalar):\n",
    "        return Vec2(scalar * self.x, scalar * self.y)\n",
    "    def __eq__(self,other):\n",
    "        return self.x == other.x and self.y == other.y\n",
    "    def __add__(self, v2):\n",
    "        return self.add(v2)\n",
    "    def __mul__(self, scalar):\n",
    "        return self.scale(scalar)\n",
    "    def __rmul__(self,scalar): #1\n",
    "        return self.scale(scalar)\n",
    "    def __repr__(self):\n",
    "        return \"Vec2({},{})\".format(self.x,self.y)"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "source": [
    "# nice repr\n",
    "3.0 * Vec2(1,0) + 4.0 * Vec2(0,1)"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Repeating the process with 3D vectors"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "source": [
    "class Vec3():\n",
    "    def __init__(self,x,y,z): #1\n",
    "        self.x = x\n",
    "        self.y = y\n",
    "        self.z = z\n",
    "    def add(self, other):\n",
    "        return Vec3(self.x + other.x, self.y + other.y, self.z + other.z)\n",
    "    def scale(self, scalar):\n",
    "        return Vec3(scalar * self.x, scalar * self.y, scalar * self.z)\n",
    "    def __eq__(self,other):\n",
    "        return self.x == other.x and self.y == other.y and self.z == other.z\n",
    "    def __add__(self, other):\n",
    "        return self.add(other)\n",
    "    def __mul__(self, scalar):\n",
    "        return self.scale(scalar)\n",
    "    def __rmul__(self,scalar):\n",
    "        return self.scale(scalar)\n",
    "    def __repr__(self):\n",
    "        return \"Vec3({},{},{})\".format(self.x,self.y, self.z)"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "source": [
    "2.0 * (Vec3(1,0,0) + Vec3(0,1,0))"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "source": [
    "def average(v1,v2):\n",
    "    return 0.5 * v1 + 0.5 * v2"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "source": [
    "average(Vec2(9.0, 1.0), Vec2(8.0,6.0))"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "source": [
    "average(Vec3(1,2,3), Vec3(4,5,6))"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Building a Vector base class"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "source": [
    "from abc import ABCMeta, abstractmethod\n",
    "\n",
    "class Vector(metaclass=ABCMeta):\n",
    "    @abstractmethod\n",
    "    def scale(self,scalar):\n",
    "        pass\n",
    "    @abstractmethod\n",
    "    def add(self,other):\n",
    "        pass"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "source": [
    "## won't work\n",
    "# v = Vector()"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "source": [
    "class Vector(metaclass=ABCMeta):\n",
    "    @abstractmethod\n",
    "    def scale(self,scalar):\n",
    "        pass\n",
    "    @abstractmethod\n",
    "    def add(self,other):\n",
    "        pass\n",
    "    def __mul__(self, scalar):\n",
    "        return self.scale(scalar)\n",
    "    def __rmul__(self, scalar):\n",
    "        return self.scale(scalar)\n",
    "    def __add__(self,other):\n",
    "        return self.add(other)"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "source": [
    "class Vec2(Vector):\n",
    "    def __init__(self,x,y):\n",
    "        self.x = x\n",
    "        self.y = y\n",
    "    def add(self,other):\n",
    "        return Vec2(self.x + other.x, self.y + other.y)\n",
    "    def scale(self,scalar):\n",
    "        return Vec2(scalar * self.x, scalar * self.y)\n",
    "    def __eq__(self,other):\n",
    "        return self.x == other.x and self.y == other.y\n",
    "    def __repr__(self):\n",
    "        return \"Vec2({},{})\".format(self.x, self.y)"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "source": [
    "# give it a subtract method\n",
    "class Vector(metaclass=ABCMeta):\n",
    "    @abstractmethod\n",
    "    def scale(self,scalar):\n",
    "        pass\n",
    "    @abstractmethod\n",
    "    def add(self,other):\n",
    "        pass\n",
    "    def __mul__(self, scalar):\n",
    "        return self.scale(scalar)\n",
    "    def __rmul__(self, scalar):\n",
    "        return self.scale(scalar)\n",
    "    def __add__(self,other):\n",
    "        return self.add(other)\n",
    "    def subtract(self,other):\n",
    "        return self.add(-1 * other)\n",
    "    def __sub__(self,other):\n",
    "        return self.subtract(other)\n",
    "    \n",
    "class Vec2(Vector):\n",
    "    def __init__(self,x,y):\n",
    "        self.x = x\n",
    "        self.y = y\n",
    "    def add(self,other):\n",
    "        return Vec2(self.x + other.x, self.y + other.y)\n",
    "    def scale(self,scalar):\n",
    "        return Vec2(scalar * self.x, scalar * self.y)\n",
    "    def __eq__(self,other):\n",
    "        return self.x == other.x and self.y == other.y\n",
    "    def __repr__(self):\n",
    "        return \"Vec2({},{})\".format(self.x, self.y)"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "source": [
    "Vec2(1,3) - Vec2(5,1)"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Unit testing vector space classes"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "source": [
    "s = -3\n",
    "u, v = Vec2(42,-10), Vec2(1.5, 8)\n",
    "s * (u + v) == s * v + s * u"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "source": [
    "from random import uniform\n",
    "\n",
    "def random_scalar():\n",
    "    return uniform(-10,10)\n",
    "\n",
    "def random_vec2():\n",
    "    return Vec2(random_scalar(),random_scalar())\n",
    "\n",
    "a = random_scalar()\n",
    "u, v = random_vec2(), random_vec2()\n",
    "## below assertion will probably fail\n",
    "# assert a * (u + v) == a * v + a * u"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "source": [
    "from math import isclose\n",
    "\n",
    "def approx_equal_vec2(v,w):\n",
    "    return isclose(v.x,w.x) and isclose(v.y,w.y) #1\n",
    "\n",
    "for _ in range(0,100): #2\n",
    "    a = random_scalar()\n",
    "    u, v = random_vec2(), random_vec2()\n",
    "    assert approx_equal_vec2(a * (u + v), a * v + a * u) #3"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "source": [
    "def test(eq, a, b, u, v, w): #<1>\n",
    "    assert eq(u + v, v + u)\n",
    "    assert eq(u + (v + w), (u + v) + w)\n",
    "    assert eq(a * (b * v), (a * b) * v)\n",
    "    assert eq(1 * v, v)\n",
    "    assert eq((a + b) * v, a * v + b * v)\n",
    "    assert eq(a * v + a * w, a * (v + w))"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "source": [
    "for i in range(0,100):\n",
    "    a,b = random_scalar(), random_scalar()\n",
    "    u,v,w = random_vec2(), random_vec2(), random_vec2()\n",
    "    test(approx_equal_vec2, a,b,u,v,w)"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercises"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**EXERCISE:** Implement a `Vec3` class inheriting from `Vector.`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "source": [
    "class Vec3(Vector):\n",
    "    def __init__(self,x,y,z):\n",
    "        self.x = x\n",
    "        self.y = y\n",
    "        self.z = z\n",
    "    def add(self,other):\n",
    "        return Vec3(self.x + other.x, self.y + other.y, self.z + other.z)\n",
    "    def scale(self,scalar):\n",
    "        return Vec3(scalar * self.x, scalar * self.y, scalar * self.z)\n",
    "    def __eq__(self,other):\n",
    "        return self.x == other.x and self.y == other.y and self.z == other.z\n",
    "    def __repr__(self):\n",
    "        return \"Vec3({},{},{})\".format(self.x, self.y, self.z)"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**MINI-PROJECT:** Implement a `CoordinateVector` class inheriting from `Vector`, with an abstract property representing the dimension.  This should save repeated work implementing specific coordinate vector classes; all you should need to do to implement a Vec6 class should be inheriting from `CoordinateVector` and setting the dimension to 6."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "source": [
    "from abc import abstractproperty\n",
    "from vectors import add, scale\n",
    "\n",
    "class CoordinateVector(Vector):\n",
    "    @abstractproperty\n",
    "    def dimension(self):\n",
    "        pass\n",
    "    def __init__(self,*coordinates):\n",
    "        self.coordinates = tuple(x for x in coordinates)\n",
    "    def add(self,other):\n",
    "        return self.__class__(*add(self.coordinates, other.coordinates))\n",
    "    def scale(self,scalar):\n",
    "        return self.__class__(*scale(scalar, self.coordinates))\n",
    "    def __repr__(self):\n",
    "        return \"{}{}\".format(self.__class__.__qualname__, self.coordinates)"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "source": [
    "class Vec6(CoordinateVector):\n",
    "    def dimension(self):\n",
    "        return 6"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {},
   "source": [
    "Vec6(1,2,3,4,5,6) + Vec6(1, 2, 3, 4, 5, 6)"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**EXERCISE:** Add a `zero` abstract method to `Vector`, designed to return the zero vector in a given vector space, as well as an implementation for the negation operator.  These are useful, because we’re required to have a zero vector and negations of any vector in a vector space."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {},
   "source": [
    "from abc import ABCMeta, abstractmethod, abstractproperty\n",
    "\n",
    "class Vector(metaclass=ABCMeta):\n",
    "    @abstractmethod\n",
    "    def scale(self,scalar):\n",
    "        pass\n",
    "    @abstractmethod\n",
    "    def add(self,other):\n",
    "        pass\n",
    "    def __mul__(self, scalar):\n",
    "        return self.scale(scalar)\n",
    "    def __rmul__(self, scalar):\n",
    "        return self.scale(scalar)\n",
    "    def __add__(self,other):\n",
    "        return self.add(other)\n",
    "    def subtract(self,other):\n",
    "        return self.add(-1 * other)\n",
    "    def __sub__(self,other):\n",
    "        return self.subtract(other)\n",
    "    @classmethod    #1\n",
    "    @abstractproperty #2\n",
    "    def zero():\n",
    "        pass\n",
    "    def __neg__(self): #3\n",
    "        return self.scale(-1)"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {},
   "source": [
    "class Vec2(Vector):\n",
    "    def __init__(self,x,y):\n",
    "        self.x = x\n",
    "        self.y = y\n",
    "    def add(self,other):\n",
    "        return Vec2(self.x + other.x, self.y + other.y)\n",
    "    def scale(self,scalar):\n",
    "        return Vec2(scalar * self.x, scalar * self.y)\n",
    "    def __eq__(self,other):\n",
    "        return self.x == other.x and self.y == other.y\n",
    "    def __repr__(self):\n",
    "        return \"Vec2({},{})\".format(self.x, self.y)\n",
    "    def zero():\n",
    "        return Vec2(0,0)"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**EXERCISE:** Write unit tests to show that the addition and scalar multiplication operations for `Vec3` satisfy the vector space properties."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {},
   "source": [
    "def random_vec3():\n",
    "    return Vec3(random_scalar(),random_scalar(),random_scalar())\n",
    "\n",
    "def approx_equal_vec3(v,w):\n",
    "    return isclose(v.x,w.x) and isclose(v.y,w.y) and isclose(v.z, w.z)\n",
    "    \n",
    "for i in range(0,100):\n",
    "    a,b = random_scalar(), random_scalar()\n",
    "    u,v,w = random_vec3(), random_vec3(), random_vec3()\n",
    "    test(approx_equal_vec3,a,b,u,v,w)"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**EXERCISE:** Add unit tests to check that $0 + \\vec{v} = \\vec{v}$, $0 \\cdot \\vec{v} = 0$, and $-\\vec{v} + \\vec{v} = 0$ for any vector $\\vec{v}$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {},
   "source": [
    "def test(zero, eq, a, b, u, v, w):\n",
    "    assert eq(u + v, v + u)\n",
    "    assert eq(u + (v + w), (u + v) + w)\n",
    "    assert eq(a * (b * v), (a * b) * v)\n",
    "    assert eq(1 * v, v)\n",
    "    assert eq((a + b) * v, a * v + b * v)\n",
    "    assert eq(a * v + a * w, a * (v + w))\n",
    "    #new tests\n",
    "    assert eq(zero + v, v)\n",
    "    assert eq(0 * v, zero)\n",
    "    assert eq(-v + v, zero)\n",
    "    \n",
    "for i in range(0,100):\n",
    "    a,b = random_scalar(), random_scalar()\n",
    "    u,v,w = random_vec2(), random_vec2(), random_vec2()\n",
    "    test(Vec2.zero(), approx_equal_vec2, a,b,u,v,w)"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**EXERCISE:** As equality is implemented above for `Vec2` and `Vec3`, it turns out that `Vec2(1,2) == Vec3(1,2,3)` returns `True`.  Python’s duck typing is too forgiving for its own good!  Fix this by adding a check that classes match before testing vector equality."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {},
   "source": [
    "class Vec2(Vector):\n",
    "    def __init__(self,x,y):\n",
    "        self.x = x\n",
    "        self.y = y\n",
    "    def add(self,other):\n",
    "        assert self.__class__ == other.__class__\n",
    "        return Vec2(self.x + other.x, self.y + other.y)\n",
    "    def scale(self,scalar):\n",
    "        return Vec2(scalar * self.x, scalar * self.y)\n",
    "    def __eq__(self,other):\n",
    "        return (self.__class__ == other.__class__\n",
    "            and self.x == other.x and self.y == other.y)\n",
    "    def __repr__(self):\n",
    "        return \"Vec2({},{})\".format(self.x, self.y)\n",
    "    def zero():\n",
    "        return Vec2(0,0)"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**EXERCISE:** Implement a `__truediv__` function on `Vector`, allowing us to divide vectors by scalars.  We can divide vectors by a non-zero scalar by multiplying them by the reciprocal of the scalar (`1.0/scalar`)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "metadata": {},
   "source": [
    "class Vector(metaclass=ABCMeta):\n",
    "    @abstractmethod\n",
    "    def scale(self,scalar):\n",
    "        pass\n",
    "    @abstractmethod\n",
    "    def add(self,other):\n",
    "        pass\n",
    "    def __mul__(self, scalar):\n",
    "        return self.scale(scalar)\n",
    "    def __rmul__(self, scalar):\n",
    "        return self.scale(scalar)\n",
    "    def __add__(self,other):\n",
    "        return self.add(other)\n",
    "    def subtract(self,other):\n",
    "        return self.add(-1 * other)\n",
    "    def __sub__(self,other):\n",
    "        return self.subtract(other)\n",
    "    @classmethod    #1\n",
    "    @abstractproperty #2\n",
    "    def zero():\n",
    "        pass\n",
    "    def __neg__(self): #3\n",
    "        return self.scale(-1)\n",
    "    def __truediv__(self, scalar):\n",
    "        return self.scale(1.0/scalar)"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "metadata": {},
   "source": [
    "class Vec2(Vector):\n",
    "    def __init__(self,x,y):\n",
    "        self.x = x\n",
    "        self.y = y\n",
    "    def add(self,other):\n",
    "        assert self.__class__ == other.__class__\n",
    "        return Vec2(self.x + other.x, self.y + other.y)\n",
    "    def scale(self,scalar):\n",
    "        return Vec2(scalar * self.x, scalar * self.y)\n",
    "    def __eq__(self,other):\n",
    "        return (self.__class__ == other.__class__\n",
    "            and self.x == other.x and self.y == other.y)\n",
    "    def __repr__(self):\n",
    "        return \"Vec2({},{})\".format(self.x, self.y)\n",
    "    def zero():\n",
    "        return Vec2(0,0)"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "metadata": {},
   "source": [
    "Vec2(1,2)/2"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Exploring different vector spaces"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Enumerating all coordinate vector spaces"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "metadata": {},
   "source": [
    "class Vec1(Vector):\n",
    "    def __init__(self,x):\n",
    "        self.x = x\n",
    "    def add(self,other):\n",
    "        return Vec1(self.x + other.x)\n",
    "    def scale(self,scalar):\n",
    "        return Vec1(scalar * self.x)\n",
    "    @classmethod\n",
    "    def zero(cls):\n",
    "        return Vec1(0)\n",
    "    def __eq__(self,other):\n",
    "        return self.x == other.x\n",
    "    def __repr__(self):\n",
    "        return \"Vec1({})\".format(self.x)"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "metadata": {},
   "source": [
    "Vec1(2) + Vec1(2)"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "metadata": {},
   "source": [
    "3 * Vec1(1)"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "metadata": {},
   "source": [
    "class Vec0(Vector):\n",
    "    def __init__(self):\n",
    "        pass\n",
    "    def add(self,other):\n",
    "        return Vec0()\n",
    "    def scale(self,scalar):\n",
    "        return Vec0()\n",
    "    @classmethod\n",
    "    def zero(cls):\n",
    "        return Vec0()\n",
    "    def __eq__(self,other):\n",
    "        return self.__class__ == other.__class__ == Vec0\n",
    "    def __repr__(self):\n",
    "        return \"Vec0()\""
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "metadata": {},
   "source": [
    "- 3.14 * Vec0()"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "metadata": {},
   "source": [
    "Vec0() + Vec0() + Vec0() + Vec0()"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Identifying vector spaces in the wild"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 42,
   "metadata": {},
   "source": [
    "class CarForSale():\n",
    "    def __init__(self, model_year, mileage, price, posted_datetime, \n",
    "                 model, source, location, description):\n",
    "        self.model_year = model_year\n",
    "        self.mileage = mileage\n",
    "        self.price = price\n",
    "        self.posted_datetime = posted_datetime\n",
    "        self.model = model\n",
    "        self.source = source\n",
    "        self.location = location\n",
    "        self.description = description"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 43,
   "metadata": {},
   "source": [
    "from datetime import datetime\n",
    "\n",
    "class CarForSale(Vector):\n",
    "    retrieved_date = datetime(2018,11,30,12) #1\n",
    "    def __init__(self, model_year, mileage, price, posted_datetime, \n",
    "                 model=\"(virtual)\", source=\"(virtual)\", #2\n",
    "                 location=\"(virtual)\", description=\"(virtual)\"):\n",
    "        self.model_year = model_year\n",
    "        self.mileage = mileage\n",
    "        self.price = price\n",
    "        self.posted_datetime = posted_datetime\n",
    "        self.model = model\n",
    "        self.source = source\n",
    "        self.location = location\n",
    "        self.description = description\n",
    "    def add(self, other):\n",
    "        def add_dates(d1, d2): #3\n",
    "            age1 = CarForSale.retrieved_date - d1\n",
    "            age2 = CarForSale.retrieved_date - d2\n",
    "            sum_age = age1 + age2\n",
    "            return CarForSale.retrieved_date - sum_age\n",
    "        return CarForSale( #4\n",
    "            self.model_year + other.model_year,\n",
    "            self.mileage + other.mileage,\n",
    "            self.price + other.price,\n",
    "            add_dates(self.posted_datetime, other.posted_datetime)\n",
    "        )\n",
    "    def scale(self,scalar):\n",
    "        def scale_date(d): #5\n",
    "            age = CarForSale.retrieved_date - d\n",
    "            return CarForSale.retrieved_date - (scalar * age)\n",
    "        return CarForSale(\n",
    "            scalar * self.model_year,\n",
    "            scalar * self.mileage,\n",
    "            scalar * self.price,\n",
    "            scale_date(self.posted_datetime)\n",
    "        )\n",
    "    @classmethod\n",
    "    def zero(cls):\n",
    "        return CarForSale(0, 0, 0, CarForSale.retrieved_date)"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 44,
   "metadata": {},
   "source": [
    "# load cargraph data from json file\n",
    "\n",
    "from json import loads, dumps\n",
    "from pathlib import Path\n",
    "from datetime import datetime\n",
    "contents = Path('cargraph.json').read_text()\n",
    "cg = loads(contents)\n",
    "cleaned = []\n",
    "\n",
    "def parse_date(s):\n",
    "    input_format=\"%m/%d - %H:%M\"\n",
    "    return datetime.strptime(s,input_format).replace(year=2018)\n",
    "    \n",
    "    return dt\n",
    "for car in cg[1:]:\n",
    "    try:\n",
    "        row = CarForSale(int(car[1]), float(car[3]), float(car[4]), parse_date(car[6]), car[2],  car[5],  car[7], car[8])\n",
    "        cleaned.append(row)\n",
    "    except: pass\n",
    "\n",
    "cars = cleaned"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 45,
   "metadata": {},
   "source": [
    "(cars[0] + cars[1]).__dict__"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "metadata": {},
   "source": [
    "average_prius = sum(cars, CarForSale.zero()) * (1.0/len(cars))"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 47,
   "metadata": {},
   "source": [
    "average_prius.__dict__"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Treating functions as vectors"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 48,
   "metadata": {},
   "source": [
    "# plotting utility function for functions in this chapter\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "from math import sin\n",
    "\n",
    "def plot(fs, xmin, xmax):\n",
    "    xs = np.linspace(xmin,xmax,100)\n",
    "    fig, ax = plt.subplots()\n",
    "    ax.axhline(y=0, color='k')\n",
    "    ax.axvline(x=0, color='k')\n",
    "    for f in fs:\n",
    "        ys = [f(x) for x in xs]\n",
    "        plt.plot(xs,ys)"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 49,
   "metadata": {},
   "source": [
    "def f(x):\n",
    "    return 0.5 * x + 3\n",
    "def g(x):\n",
    "    return sin(x)\n",
    "plot([f,g],-10,10)"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 50,
   "metadata": {},
   "source": [
    "def add_functions(f,g):\n",
    "    def new_function(x):\n",
    "        return f(x) + g(x)\n",
    "    return new_function"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Treating matrices as vectors"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 51,
   "metadata": {},
   "source": [
    "class Matrix5_by_3(Vector):\n",
    "    rows = 5 #1\n",
    "    columns = 3\n",
    "    def __init__(self, matrix):\n",
    "        self.matrix = matrix\n",
    "    def add(self, other):\n",
    "        return Matrix5_by_3(tuple(\n",
    "            tuple(a + b for a,b in zip(row1, row2))\n",
    "            for (row1, row2) in zip(self.matrix, other.matrix)\n",
    "        ))\n",
    "    def scale(self,scalar):\n",
    "        return Matrix5_by_3(tuple(\n",
    "            tuple(scalar * x for x in row)\n",
    "            for row in self.matrix\n",
    "        ))\n",
    "    @classmethod\n",
    "    def zero(cls):\n",
    "        return Matrix5_by_3(tuple( #2\n",
    "            tuple(0 for j in range(0, cls.columns))\n",
    "            for i in range(0, cls.rows)\n",
    "        ))"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Manipulating images with vector operations"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 52,
   "metadata": {},
   "source": [
    "from PIL import Image\n",
    "class ImageVector(Vector):\n",
    "    size = (300,300) #1\n",
    "    def __init__(self,input):\n",
    "        try:\n",
    "            img = Image.open(input).resize(ImageVector.size) #2\n",
    "            self.pixels = img.getdata()\n",
    "        except:\n",
    "            self.pixels = input #3\n",
    "    def image(self):\n",
    "        img = Image.new('RGB', ImageVector.size) #4\n",
    "        img.putdata([(int(r), int(g), int(b)) \n",
    "                     for (r,g,b) in self.pixels])\n",
    "        return img\n",
    "    def add(self,img2): #5\n",
    "        return ImageVector([(r1+r2,g1+g2,b1+b2) \n",
    "                            for ((r1,g1,b1),(r2,g2,b2)) \n",
    "                            in zip(self.pixels,img2.pixels)])\n",
    "    def scale(self,scalar): #6\n",
    "        return ImageVector([(scalar*r,scalar*g,scalar*b) \n",
    "                      for (r,g,b) in self.pixels])\n",
    "    @classmethod\n",
    "    def zero(cls): #7\n",
    "        total_pixels = cls.size[0] * cls.size[1]\n",
    "        return ImageVector([(0,0,0) for _ in range(0,total_pixels)])\n",
    "    def _repr_png_(self): #8\n",
    "        return self.image()._repr_png_()"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 53,
   "metadata": {},
   "source": [
    "0.5 * ImageVector(\"inside.JPG\") + 0.5 * ImageVector(\"outside.JPG\")"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 54,
   "metadata": {},
   "source": [
    "white = ImageVector([(255,255,255) for _ in range(0,300*300)])"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 55,
   "metadata": {},
   "source": [
    "ImageVector(\"melba_toy.JPG\")"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 56,
   "metadata": {},
   "source": [
    "white - ImageVector(\"melba_toy.JPG\")"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercies"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**EXERCISE:** Run the vector space unit tests with `u`, `v`, and `w` as floats rather than objects inheriting Vector.  This demonstrates that real numbers are indeed vectors."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 57,
   "metadata": {},
   "source": [
    "for i in range(0,100):\n",
    "    a,b = random_scalar(), random_scalar()\n",
    "    u,v,w = random_scalar(), random_scalar(), random_scalar()\n",
    "    test(0, isclose, a,b,u,v,w)"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**EXERCISE:** What is the zero vector for a `CarForSale`?  Implement the `CarForSale.zero()` function to make it available."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 58,
   "metadata": {},
   "source": [
    "class CarForSale(Vector):\n",
    "    retrieved_date = datetime(2018,11,30,12) #1\n",
    "    def __init__(self, model_year, mileage, price, posted_datetime, \n",
    "                 model=\"(virtual)\", source=\"(virtual)\", #2\n",
    "                 location=\"(virtual)\", description=\"(virtual)\"):\n",
    "        self.model_year = model_year\n",
    "        self.mileage = mileage\n",
    "        self.price = price\n",
    "        self.posted_datetime = posted_datetime\n",
    "        self.model = model\n",
    "        self.source = source\n",
    "        self.location = location\n",
    "        self.description = description\n",
    "    def add(self, other):\n",
    "        def add_dates(d1, d2): #3\n",
    "            age1 = CarForSale.retrieved_date - d1\n",
    "            age2 = CarForSale.retrieved_date - d2\n",
    "            sum_age = age1 + age2\n",
    "            return CarForSale.retrieved_date - sum_age\n",
    "        return CarForSale( #4\n",
    "            self.model_year + other.model_year,\n",
    "            self.mileage + other.mileage,\n",
    "            self.price + other.price,\n",
    "            add_dates(self.posted_datetime, other.posted_datetime)\n",
    "        )\n",
    "    def scale(self,scalar):\n",
    "        def scale_date(d): #5\n",
    "            age = CarForSale.retrieved_date - d\n",
    "            return CarForSale.retrieved_date - (scalar * age)\n",
    "        return CarForSale(\n",
    "            scalar * self.model_year,\n",
    "            scalar * self.mileage,\n",
    "            scalar * self.price,\n",
    "            scale_date(self.posted_datetime)\n",
    "        )\n",
    "    @classmethod\n",
    "    def zero(cls):\n",
    "        return CarForSale(0, 0, 0, CarForSale.retrieved_date)"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**MINI-PROJECT:** Run the vector space unit tests for `CarForSale` to show its objects form a vector space (ignoring their textual attributes)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 59,
   "metadata": {},
   "source": [
    "from math import isclose\n",
    "from random import uniform, random, randint\n",
    "from datetime import datetime, timedelta\n",
    "\n",
    "def random_time():\n",
    "    return CarForSale.retrieved_date - timedelta(days=uniform(0,10))\n",
    "\n",
    "def approx_equal_time(t1, t2):\n",
    "    test = datetime.now()\n",
    "    return isclose((test-t1).total_seconds(), (test-t2).total_seconds())\n",
    "\n",
    "def random_car():\n",
    "    return CarForSale(randint(1990,2019), randint(0,250000), \n",
    "              27000. * random(), random_time())\n",
    "\n",
    "def approx_equal_car(c1,c2):\n",
    "    return (isclose(c1.model_year,c2.model_year) \n",
    "            and isclose(c1.mileage,c2.mileage) \n",
    "            and isclose(c1.price, c2.price)\n",
    "            and approx_equal_time(c1.posted_datetime, c2.posted_datetime))\n",
    "    \n",
    "for i in range(0,100):\n",
    "    a,b = random_scalar(), random_scalar()\n",
    "    u,v,w = random_car(), random_car(), random_car()\n",
    "    test(CarForSale.zero(), approx_equal_car, a,b,u,v,w)"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**EXERCISE:** Implement class `Function(Vector)` that takes a function of 1 variable as an argument to its constructor, with a `__call__` implemented so we can treat it as a function.  You should be able to run `plot([f,g,f+g,3*g],-10,10)`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 60,
   "metadata": {},
   "source": [
    "class Function(Vector):\n",
    "    def __init__(self, f):\n",
    "        self.function = f\n",
    "    def add(self, other):\n",
    "        return Function(lambda x: self.function(x) + other.function(x))\n",
    "    def scale(self, scalar):\n",
    "        return Function(lambda x: scalar * self.function(x))\n",
    "    @classmethod\n",
    "    def zero(cls):\n",
    "        return Function(lambda x: 0)\n",
    "    def __call__(self, arg):\n",
    "        return self.function(arg)"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 61,
   "metadata": {},
   "source": [
    "f = Function(lambda x: 0.5 * x + 3)\n",
    "g = Function(sin)"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 62,
   "metadata": {},
   "source": [
    "plot([f, g, f+g, 3*g], -10, 10)"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**MINI-PROJECT:** Testing equality of functions is difficult.  Do your best to write a function to test whether two functions are equal."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 63,
   "metadata": {},
   "source": [
    "def approx_equal_function(f,g):\n",
    "    results = []\n",
    "    for _ in range(0,10):\n",
    "        x = uniform(-10,10)\n",
    "        results.append(isclose(f(x),g(x)))\n",
    "    return all(results)"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 64,
   "metadata": {},
   "source": [
    "approx_equal_function(lambda x: (x*x)/x, lambda x: x)"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**MINI-PROJECT:** Unit test your Function class to demonstrate that functions satisfy the vector space properties."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 65,
   "metadata": {},
   "source": [
    "class Polynomial(Vector):\n",
    "    def __init__(self, *coefficients):\n",
    "        self.coefficients = coefficients\n",
    "    def __call__(self,x):\n",
    "        return sum(coefficient * x ** power for (power,coefficient) in enumerate(self.coefficients))\n",
    "    def add(self,p):\n",
    "        return Polynomial([a + b for a,b in zip(self.coefficients, p.coefficients)])\n",
    "    def scale(self,scalar):\n",
    "        return Polynomial([scalar * a for a in self.coefficients])\n",
    "    def _repr_latex_(self):\n",
    "        monomials = [repr(coefficient) if power == 0 \n",
    "                               else \"x ^ {%d}\" % power if coefficient == 1\n",
    "                               else \"%s x ^ {%d}\" % (coefficient,power) \n",
    "                               for (power,coefficient) in enumerate(self.coefficients) \n",
    "                               if coefficient != 0]\n",
    "        return \"$ %s $\" % (\" + \".join(monomials))\n",
    "    @classmethod\n",
    "    def zero(cls):\n",
    "        return Polynomial(0)\n",
    "\n",
    "def random_function():\n",
    "    degree = randint(0,5)\n",
    "    p = Polynomial(*[uniform(-10,10) for _ in range(0,degree)])\n",
    "    return Function(lambda x: p(x))\n",
    "\n",
    "for i in range(0,100):\n",
    "    a,b = random_scalar(), random_scalar()\n",
    "    u,v,w = random_function(), random_function(), random_function()\n",
    "    test(Function.zero(), approx_equal_function, a,b,u,v,w)"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**MINI-PROJECT:** Implement a class `Function2(Vector)` that stores a function of two variables, like $f(x,y) = x + y.$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 66,
   "metadata": {},
   "source": [
    "class Function(Vector):\n",
    "    def __init__(self, f):\n",
    "        self.function = f\n",
    "    def add(self, other):\n",
    "        return Function(lambda x,y: self.function(x,y) + other.function(x,y))\n",
    "    def scale(self, scalar):\n",
    "        return Function(lambda x,y: scalar * self.function(x,y))\n",
    "    @classmethod\n",
    "    def zero(cls):\n",
    "        return Function(lambda x,y: 0)\n",
    "    def __call__(self, *args):\n",
    "        return self.function(*args)"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 67,
   "metadata": {},
   "source": [
    "f = Function(lambda x,y:x+y)\n",
    "g = Function(lambda x,y: x-y+1)\n",
    "(f+g)(3,10)"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**MINI-PROJECT:** Implement a `Matrix` class inheriting from `Vector` with abstract properties representing number of rows and number of columns.  You should not be able to instantiate a Matrix class, but you could make a `Matrix5_by_3` by inheriting from Matrix and specifying the number of rows and columns explicitly."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 68,
   "metadata": {},
   "source": [
    "class Matrix(Vector):\n",
    "    @abstractproperty\n",
    "    def rows(self):\n",
    "        pass\n",
    "    @abstractproperty\n",
    "    def columns(self):\n",
    "        pass\n",
    "    def __init__(self,entries):\n",
    "        self.entries = entries\n",
    "    def add(self,other):\n",
    "        return self.__class__(\n",
    "            tuple(\n",
    "                tuple(self.entries[i][j] + other.entries[i][j]\n",
    "                        for j in range(0,self.columns()))\n",
    "                for i in range(0,self.rows())))\n",
    "    def scale(self,scalar):\n",
    "        return self.__class__(\n",
    "            tuple(\n",
    "                tuple(scalar * e for e in row) \n",
    "                for row in self.entries))\n",
    "    def __repr__(self):\n",
    "        return \"%s%r\" % (self.__class__.__qualname__, self.entries)\n",
    "    def zero(self):\n",
    "        return self.__class__(\n",
    "            tuple(\n",
    "                tuple(0 for i in range(0,self.columns())) \n",
    "                for j in range(0,self.rows())))"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 69,
   "metadata": {},
   "source": [
    "class Matrix2_by_2(Matrix):\n",
    "    def rows(self):\n",
    "        return 2\n",
    "    def columns(self):\n",
    "        return 2  "
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 70,
   "metadata": {},
   "source": [
    "2 * Matrix2_by_2(((1,2),(3,4))) + Matrix2_by_2(((1,2),(3,4)))"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**EXERCISE:** Unit test the `Matrix5_by_3` class to demonstrate that it obeys the defining properties of a vector space."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 71,
   "metadata": {},
   "source": [
    "def random_matrix(rows, columns):\n",
    "    return tuple(\n",
    "        tuple(uniform(-10,10) for j in range(0,columns))\n",
    "        for i in range(0,rows)\n",
    "    )\n",
    "\n",
    "def random_5_by_3():\n",
    "    return Matrix5_by_3(random_matrix(5,3))\n",
    "    \n",
    "def approx_equal_matrix_5_by_3(m1,m2):\n",
    "    return all([\n",
    "        isclose(m1.matrix[i][j],m2.matrix[i][j]) \n",
    "        for j in range(0,3)\n",
    "        for i in range(0,5)\n",
    "    ])\n",
    "    \n",
    "for i in range(0,100):\n",
    "    a,b = random_scalar(), random_scalar()\n",
    "    u,v,w = random_5_by_3(), random_5_by_3(), random_5_by_3()\n",
    "    test(Matrix5_by_3.zero(), approx_equal_matrix_5_by_3, a,b,u,v,w)"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**EXERCISE:** Convince yourself that the “`zero`” vector for the `ImageVector` class doesn’t visibly alter any image when it is added."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 72,
   "metadata": {},
   "source": [
    "ImageVector(\"melba_toy.JPG\")"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 73,
   "metadata": {},
   "source": [
    "# unchanged\n",
    "ImageVector(\"melba_toy.JPG\") + ImageVector.zero()"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**EXERCISE:** Pick two images and display 10 different weighted averages of them.  These will be “points on a line segment” connecting the images in 270,000-dimensional space!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 74,
   "metadata": {},
   "source": [
    "linear_combos = [s * ImageVector(\"inside.JPG\") + (1-s) * ImageVector(\"outside.JPG\")\n",
    "                    for s in [0.1*i for i in range(0,11)]]\n",
    "\n",
    "# e.g.\n",
    "linear_combos[6]"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**EXERCISE:** Adapt the vector space unit tests to images, and run them.  What do your randomized unit tests look like as images?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 75,
   "metadata": {},
   "source": [
    "def random_image():\n",
    "    return ImageVector([(randint(0,255), randint(0,255), randint(0,255))\n",
    "                            for i in range(0,300 * 300)])"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 76,
   "metadata": {},
   "source": [
    "random_image()"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 77,
   "metadata": {},
   "source": [
    "def approx_equal_image(i1,i2):\n",
    "    return all([isclose(c1,c2)\n",
    "        for p1,p2 in zip(i1.pixels,i2.pixels)\n",
    "        for c1,c2 in zip(p1,p2)])"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 80,
   "metadata": {},
   "source": [
    "## takes a while to run, but succeeds\n",
    "# for i in range(0,100):\n",
    "#     a,b = random_scalar(), random_scalar()\n",
    "#     u,v,w = random_image(), random_image(), random_image()\n",
    "#     test(ImageVector.zero(), approx_equal_image, a,b,u,v,w)"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Looking for smaller vector spaces"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Finding subspaces of the vector space of functions"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 81,
   "metadata": {},
   "source": [
    "class LinearFunction(Vector):\n",
    "    def __init__(self,a,b):\n",
    "        self.a = a\n",
    "        self.b = b\n",
    "    def add(self,v):\n",
    "        return LinearFunction(self.a + v.a, self.b + v.b)\n",
    "    def scale(self,scalar):\n",
    "        return LinearFunction(scalar * self.a, scalar * self.b)\n",
    "    def __call__(self,x):\n",
    "        return self.a * x + self.b\n",
    "    @classmethod\n",
    "    def zero(cls):\n",
    "        return LinearFunction(0,0,0)"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 82,
   "metadata": {},
   "source": [
    "plot([LinearFunction(-2,2)], -5, 5)"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 83,
   "metadata": {},
   "source": [
    "gray = ImageVector([(1,1,1) for _ in range(0,300*300)])"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 84,
   "metadata": {},
   "source": [
    "gray"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 85,
   "metadata": {},
   "source": [
    "63*gray"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 86,
   "metadata": {},
   "source": [
    "127*gray"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 87,
   "metadata": {},
   "source": [
    "191*gray"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 88,
   "metadata": {},
   "source": [
    "255*gray"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exercises"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**EXERCISE:** Rebuild the `LinearFunction` class by inheriting from `Vec2` and simply implementing the `__call__` method."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 91,
   "metadata": {},
   "source": [
    "class LinearFunction(Vec2):\n",
    "    def __call__(self,input):\n",
    "        return self.x * input + self.y"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**MINI-PROJECT:** Implement a class `QuadraticFunction(Vector)` representing the vector subspace of functions of the form $ax^2 + bx + c$.  What is a basis for this subspace?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 92,
   "metadata": {},
   "source": [
    "class QuadraticFunction(Vector):\n",
    "    def __init__(self,a,b,c):\n",
    "        self.a = a\n",
    "        self.b = b\n",
    "        self.c = c\n",
    "    def add(self,v):\n",
    "        return QuadraticFunction(self.a + v.a, self.b + v.b, self.c + v.c)\n",
    "    def scale(self,scalar):\n",
    "        return QuadraticFunction(scalar * self.a, scalar * self.b, scalar * self.c)\n",
    "    def __call__(self,x):\n",
    "        return self.a * x * x + self.b * x + self.c\n",
    "    @classmethod\n",
    "    def zero(cls):\n",
    "        return QuadraticFunction(0,0,0)"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**MINI-PROJECT:** Vector space of all polynomials is an infinite-dimensional subspace -- implement it and describe a basis (which will need to be an infinite set!)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 93,
   "metadata": {},
   "source": [
    "class Polynomial(Vector):\n",
    "    def __init__(self, *coefficients):\n",
    "        self.coefficients = coefficients\n",
    "    def __call__(self,x):\n",
    "        return sum(coefficient * x ** power for (power,coefficient) in enumerate(self.coefficients))\n",
    "    def add(self,p):\n",
    "        return Polynomial([a + b for a,b in zip(self.coefficients, p.coefficients)])\n",
    "    def scale(self,scalar):\n",
    "        return Polynomial([scalar * a for a in self.coefficients])\n",
    "        return \"$ %s $\" % (\" + \".join(monomials))\n",
    "    @classmethod\n",
    "    def zero(cls):\n",
    "        return Polynomial(0)"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**EXERCISE:** Write a function `solid_color(r,g,b)` that returns an solid color `ImageVector` with the given red, green, and blue content at every pixel."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 94,
   "metadata": {},
   "source": [
    "def solid_color(r,g,b):\n",
    "    return ImageVector([(r,g,b) for _ in range(0,300*300)])"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 97,
   "metadata": {},
   "source": [
    "solid_color(250,100,100)"
   ],
   "outputs": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**MINI-PROJECT:** Write a linear map that generates an `ImageVector` from a 30 by 30 grayscale image, implemented as a 30 by 30 matrix of brightness values.  Then, implement the linear map that takes a 300 by 300 image to a 30 by 30 grayscale image by averaging the brightness (average of red, green and blue) at each pixel."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 106,
   "metadata": {},
   "source": [
    "image_size = (300,300)\n",
    "total_pixels = image_size[0] * image_size[1]\n",
    "square_count = 30 #<1>\n",
    "square_width = 10\n",
    "\n",
    "def ij(n):\n",
    "    return (n // image_size[0], n % image_size[1])\n",
    "\n",
    "def to_lowres_grayscale(img): #<2>\n",
    "\n",
    "    matrix = [\n",
    "        [0 for i in range(0,square_count)]\n",
    "        for j in range(0,square_count)\n",
    "    ]\n",
    "    for (n,p) in enumerate(img.pixels):\n",
    "        i,j = ij(n)\n",
    "        weight = 1.0 / (3 * square_width * square_width)\n",
    "        matrix[i // square_width][ j // square_width] += (sum(p) * weight)\n",
    "    return matrix\n",
    "\n",
    "def from_lowres_grayscale(matrix): #<3>\n",
    "    def lowres(pixels, ij):\n",
    "        i,j = ij\n",
    "        return pixels[i // square_width][ j // square_width]\n",
    "    def make_highres(limg):\n",
    "        pixels = list(matrix)\n",
    "        triple = lambda x: (x,x,x)\n",
    "        return ImageVector([triple(lowres(matrix, ij(n))) for n in range(0,total_pixels)])\n",
    "    return make_highres(matrix)"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 107,
   "metadata": {},
   "source": [
    "v = ImageVector(\"melba_toy.JPG\")"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": 108,
   "metadata": {},
   "source": [
    "# a 30x30 list of numbers\n",
    "lowres = to_lowres_grayscale(v)\n",
    "from_lowres_grayscale(lowres)"
   ],
   "outputs": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "source": [],
   "outputs": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
